using System;
using System.Collections;
using System.Data;
using System.Security.Cryptography;

using FreeFlow.Client;
using System.Collections.Generic;

namespace FreeFlow.Administration
{
	/// <summary>
	/// A registered user.
	/// </summary>
	public class User : DatabaseAware, IUpdatable, IDeletable, IRefreshable
	{
    internal const string EncodedBlankPassword = "D41D8CD98F00B204E9800998ECF8427E";
    private List<string> roles;
    internal User(Server server, IDataReader reader) : base(server)
    {
      LoadFromReader(reader);
    }

    internal User(Server server) : base(server)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="User"/> class for the specified user.
    /// </summary>
    /// <param name="server">The server.</param>
    /// <param name="userName">Name of the user.</param>
    public User(Server server, string userName) : base(server)
    {
      LoadForUser(userName);
    }

    private void LoadForUser(string userName)
    {
      using (IDataReader reader = Server.ExecuteReader(string.Format(
        "SELECT * FROM eUser WHERE eUserName='{0}'", SqlEscape(userName))))
      {
        if (reader.Read())
          LoadFromReader(reader);
        else
          throw new ItemDeletedException(string.Format("The user {0} does not exist", userName));
      }
    }

    private void LoadFromReader(IDataReader reader)
    {
      name = reader["eUserName"].ToString();
      oldName = name;
      emailAddress = reader["eEMailAddress"].ToString();
      reportsTo = reader["eReportsTo"].ToString();
      distinguishedName = reader["eDistinguishedName"].ToString();
      hasBlankPassword = (reader["ePassword"].ToString() == EncodedBlankPassword);
      if (reader["eDeliverAlertsByEMail"] != System.DBNull.Value)
        deliverAlertsByEmail = Convert.ToBoolean(reader["eDeliverAlertsByEMail"]);
      directoryTree = reader["eDirectoryTree"].ToString();
		}

    // get roles for this user
    private void ReadRoles()
    {
      if (roles == null)
      {
        roles = new List<string>();
        using (IDataReader reader = Server.ExecuteReader(
          string.Format("SELECT eRoleName FROM eAssignment WHERE eUserName = '{0}' AND eRoleName != 'everybody' AND eRoleName != '{0}'", SqlEscape(oldName))))
        {
          while (reader.Read())
          {
            roles.Add(reader["eRoleName"].ToString());
          }
        }
      }
    }

    #region properties
    private string name = string.Empty;
    private string oldName = string.Empty;
    /// <summary>
    /// Gets or sets the name of the user.
    /// </summary>
    public string Name
    {
      get
      {
        return name;
      }
      set
      {
        name = value;
      }
    }

    private string emailAddress = string.Empty;
    /// <summary>
    /// Gets and sets the email address for this user.
    /// </summary>
    public string EmailAddress
    {
      get
      {
        return emailAddress;
      }
      set
      {
        emailAddress = value;
      }
    }

    private string reportsTo = string.Empty;
    /// <summary>
    /// Gets or sets the user name that this user reports to.
    /// </summary>
    public string ReportsTo
    {
      get
      {
        return reportsTo;
      }
      set
      {
        reportsTo = value;
      }
    }

    private string distinguishedName;
    /// <summary>
    /// Gets or sets the distinguished name of the user.
    /// </summary>
    public string DistinguishedName
    {
      get
      {
        return distinguishedName;
      }
      set
      {
        distinguishedName = value;
      }
    }

    private bool hasBlankPassword;
    /// <summary>
    /// Gets a value indicating whether this user has a blank password.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if this user has a blank password; otherwise, <c>false</c>.
    /// </value>
    public bool HasBlankPassword
    {
      get
      {
        return hasBlankPassword;
      }
    }

    private bool deliverAlertsByEmail;
    /// <summary>
    /// Gets or sets a value indicating whether to deliver alerts to this user by email.
    /// </summary>
    /// <value>
    /// 	<c>true</c> if alerts should delivered by email; otherwise, <c>false</c>.
    /// </value>
    public bool DeliverAlertsByEmail
    {
      get
      {
        return deliverAlertsByEmail;
      }
      set
      {
        deliverAlertsByEmail = value;
      }
    }

    private string directoryTree;
    /// <summary>
    /// Gets or sets the directory tree for this user.
    /// </summary>
    /// <value>The directory tree for this user.</value>
    public string DirectoryTree
    {
      get
      {
        return directoryTree;
      }
      set
      {
        directoryTree = value;
      }
    }

    #endregion

    /// <summary>
    /// Applies any changes made to this user.
    /// </summary>
    public void ApplyChanges()
    {
      if (string.IsNullOrEmpty(name))
        throw new FreeFlowException("The user's name can not be blank");

      bool newUser = false;
      // check new name doesn't conflict with an existing one
      using (IDataReader reader = Server.ExecuteReader(string.Format(
        "SELECT * FROM eUser WHERE LOWER(eUserName) = '{0}'", SqlEscape(name))))
      {
        if (reader.Read())
        {
          if (oldName != name)
          {
            FreeFlowException e = new FreeFlowException(string.Format("A user with the name {0} already exists", name));
            name = oldName;
            throw e;
          }
        }
        else if (string.IsNullOrEmpty(oldName))
        {
          // this is a new user
          newUser = true;
        }
      }

      ReadRoles();
      using (IDbTransaction transaction = Server.BeginTransaction())
      {
        // add new user
        if (newUser)
          Server.ExecuteNonQuery(transaction, 
            string.Format("INSERT INTO eUser (eUserName) VALUES ('{0}')", SqlEscape(name)));

        // roles
        Server.ExecuteNonQuery(transaction, 
          string.Format("DELETE FROM eAssignment WHERE eUserName = '{0}'", SqlEscape(oldName)));

        for (int i = 0; i < roles.Count; i++)
        {
          Server.ExecuteNonQuery(transaction, 
            string.Format("INSERT INTO eAssignment (eUserName, eRoleName, eFolderID) VALUES ('{0}', '{1}', ' ')", 
            SqlEscape(name), SqlEscape(roles[i].ToString())));
        }
        Server.ExecuteNonQuery(transaction, 
          string.Format("INSERT INTO eAssignment (eUserName, eRoleName, eFolderID) VALUES ('{0}', 'everybody', ' ')", 
          SqlEscape(name)));
        Server.ExecuteNonQuery(transaction, 
          string.Format("INSERT INTO eAssignment (eUserName, eRoleName, eFolderID) VALUES ('{0}', '{0}', ' ')", 
          SqlEscape(name)));

        // name change
        if ((name != oldName) && (!newUser))
        {
          // eAssignment will be handled above
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eUser SET eUserName='{0}' WHERE eUserName='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eUser SET eReportsTo='{0}' WHERE eReportsTo='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eAttribute SET eUserName='{0}' WHERE eUserName='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eAlert SET eUserName='{0}' WHERE eUserName='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eFolder SET eOriginator='{0}' WHERE eOriginator='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eFolder SET eActionUser='{0}' WHERE eActionUser='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eSession SET eUserName='{0}' WHERE eUserName='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eLog SET eUserName='{0}' WHERE eUserName='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
          Server.ExecuteNonQuery(transaction, string.Format("UPDATE eEvent SET eUserName='{0}' WHERE eUserName='{1}'", 
            SqlEscape(name), SqlEscape(oldName)));
        }

        Server.ExecuteNonQuery(transaction, 
          string.Format("UPDATE eUser SET eEMailAddress='{0}', eReportsTo='{1}', eDistinguishedName='{2}', " +
          "eDeliverAlertsByEmail={3}, eDirectoryTree='{4}' WHERE eUserName = '{5}'", 
          SqlEscape(emailAddress), SqlEscape(reportsTo), SqlEscape(distinguishedName), 
          Convert.ToInt32(deliverAlertsByEmail), SqlEscape(directoryTree), SqlEscape(name)));

        transaction.Commit();
      }

      oldName = name;
    }

    /// <summary>
    /// Determines whether the user has the specified role.
    /// </summary>
    /// <param name="roleName">Name of the role.</param>
    /// <returns>
    /// 	<c>true</c> if the user has the role; otherwise, <c>false</c>.
    /// </returns>
    public bool HasRole(string roleName)
    {
      ReadRoles();
      return roles.Contains(roleName);
    }

    /// <summary>
    /// Grants the specified role to this user. This change will not take effect until a call to ApplyChanges has been made.
    /// </summary>
    /// <param name="roleName">Name of the role.</param>
    public void GrantRole(string roleName)
    {
      if (!roles.Contains(roleName))
        roles.Add(roleName);
    }

    /// <summary>
    /// Revokes the specified role from the user. This change will not take effect until a call to ApplyChanges has been made.
    /// </summary>
    /// <param name="roleName">Name of the role.</param>
    public void RevokeRole(string roleName)
    {
      roles.Remove(roleName);
    }

    /// <summary>
    /// Revokes all roles from the user. This change will not take effect until a call to ApplyChanges has been made.
    /// </summary>
    public void RevokeAllRoles()
    {
      roles.Clear();
    }

    #region IDeletable Members

    /// <summary>
    /// Deletes this user from the database.
    /// </summary>
    public void Delete()
    {
      using (IDbTransaction transaction = Server.BeginTransaction())
      {
        Server.ExecuteNonQuery(transaction, string.Format("DELETE FROM eUser WHERE eUserName='{0}'", SqlEscape(name)));
        Server.ExecuteNonQuery(transaction, string.Format("DELETE FROM eAssignment WHERE eUserName='{0}'", SqlEscape(name)));
        Server.ExecuteNonQuery(transaction, string.Format("DELETE FROM eAlert WHERE eUserName='{0}'", SqlEscape(name)));
        Server.ExecuteNonQuery(transaction, string.Format("DELETE FROM eSession WHERE eUserName='{0}'", SqlEscape(name)));
        transaction.Commit();
      }
    }

    #endregion
  
    private DesignerLog designerLog;
    /// <summary>
    /// Gets the designer log entries for this user.
    /// </summary>
    public DesignerLog DesignerLog
    {
      get
      {
        if (designerLog == null)
          designerLog = new DesignerLog(Server, this);
        return designerLog;
      }
    }

    private History history;
    /// <summary>
    /// Gets the history for this user.
    /// </summary>
    public History History
    {
      get
      {
        if (history == null)
          history = new History(Server, this);
        return history;
      }
    }

    private AlertCollection toDoList;
    /// <summary>
    /// Gets the To Do list for this user.
    /// </summary>
    public AlertCollection ToDoList
    {
      get
      {
        if (toDoList == null)
          toDoList = new AlertCollection(Server, this, AlertType.ToDo);
        return toDoList;
      }
    }

    private AlertCollection watchList;
    /// <summary>
    /// Gets the Watch list for this user.
    /// </summary>
    public AlertCollection WatchList
    {
      get
      {
        if (watchList == null)
          watchList = new AlertCollection(Server, this, AlertType.Watch);
        return watchList;
      }
    }

    private UserAttributeCollection attributes;
    /// <summary>
    /// Gets the attributes for this user.
    /// </summary>
    public UserAttributeCollection Attributes
    {
      get
      {
        if (attributes == null)
          attributes = new UserAttributeCollection(Server, name);
        return attributes;
      }
    }


    /// <summary>
    /// Sets the user's password.
    /// </summary>
    /// <param name="password">The new password</param>
    public void SetPassword(string password)
    {
      string encodedPassword = Connection.MD5Encode(password);
      Server.ExecuteNonQuery(
        string.Format("UPDATE eUser SET ePassword='{0}' WHERE eUserName = '{1}'", 
        encodedPassword, SqlEscape(oldName)));
      hasBlankPassword = (encodedPassword == EncodedBlankPassword);
    }

    /// <summary>
    /// Sets a random password for the user.
    /// </summary>
    /// <param name="passwordLength">Length of the password.</param>
    /// <returns>The new password.</returns>
    public string SetRandomPassword(int passwordLength)
    {
      string _allowedChars = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ23456789";
      byte[] randomBytes = new Byte[passwordLength];
      RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
      rng.GetBytes(randomBytes);
      char[] chars = new char[passwordLength];
      int allowedCharCount = _allowedChars.Length;

      for(int i = 0;i<passwordLength;i++)
      {
        chars[i] = _allowedChars[(int)randomBytes[i] % allowedCharCount];
      }

      string password = new string(chars);
      SetPassword(password);
      return password;
    }

    /// <summary>
    /// Sets a random password for the user.
    /// </summary>
    /// <returns>The new password.</returns>
    public string SetRandomPassword()
    {
      return SetRandomPassword(8);
    }

    #region IRefreshable Members

    /// <summary>
    /// Refreshes the data for this user.
    /// </summary>
    public void Refresh()
    {
      LoadForUser(Name);
      roles = null;
      designerLog = null;
      history = null;
      toDoList = null;
      watchList = null;
      attributes = null;
    }

    #endregion
  }
}
